Skip to content

readline: improve performance#64585

Closed
mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:readline-createinterface-perf
Closed

readline: improve performance#64585
mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:readline-createinterface-perf

Conversation

@mcollina

@mcollina mcollina commented Jul 18, 2026

Copy link
Copy Markdown
Member

Speed up readline.createInterface(), which is on the hot path of every CLI tool that reads lines from a stream.

A CPU profile of interface construction showed three avoidable costs:

  1. setupHistoryManager() accounted for ~43% of construction self time: every instance ran four ObjectDefineProperty calls, allocating six fresh closures and four descriptor objects each time. The accessors (history, historyIndex, historySize, isFlushing) are now defined from a module-level shared descriptor map with a single ObjectDefineProperties call. They remain own, enumerable, configurable properties, so observable semantics are unchanged.
  2. The constructor assigned size, history and removeHistoryDuplicates onto the user-provided input stream to forward them to the history manager. It now passes a plain options object instead, so the user's stream is no longer observably mutated and doesn't go through extra hidden-class transitions. The legacy positional-arguments path is unchanged.
  3. The Interface wrapper unconditionally read process.env.TERM (which goes through the env interceptor) to bind the dumb-terminal _ttyWrite. The check is now gated on this.terminal, since _ttyWrite is never invoked when the interface is not in terminal mode.

Results with the new benchmark (30 runs, interleaved via benchmark/compare.js):

                                                         confidence improvement accuracy (*)   (**)  (***)
readline/readline-createInterface.js terminal=0 n=100000        ***     63.81 %       ±1.94% ±2.59% ±3.38%
readline/readline-createInterface.js terminal=1 n=100000        ***     38.05 %       ±1.90% ±2.53% ±3.31%

Update: a second commit speeds up line throughput as well. [kNormalWrite] ran a regular-expression exec loop that allocated a match object and two index entries per line, then sliced each line out in JS. It now does a single split pass, and uses a plain string split on "\n" when the chunk contains none of the rare line endings (\r, , ), which takes V8's fast string-split path.

Results for readline/readline-iterable.js (20 runs, interleaved):

                                                   confidence improvement accuracy (*)    (**)   (***)
readline/readline-iterable.js type='new' n=10             ***      5.18 %       ±1.91%  ±2.57%  ±3.38%
readline/readline-iterable.js type='new' n=100              *     13.16 %      ±12.52% ±16.89% ±22.48%
readline/readline-iterable.js type='new' n=1000           ***     32.94 %      ±11.25% ±15.31% ±20.66%
readline/readline-iterable.js type='new' n=10000          ***     47.44 %       ±4.83%  ±6.53%  ±8.70%
readline/readline-iterable.js type='new' n=100000         ***     70.71 %       ±2.18%  ±2.94%  ±3.91%
readline/readline-iterable.js type='new' n=1000000        ***     80.62 %       ±1.51%  ±2.05%  ±2.75%
readline/readline-iterable.js type='old' n=10             ***      4.92 %       ±1.50%  ±2.01%  ±2.66%
readline/readline-iterable.js type='old' n=100                    -5.79 %      ±10.40% ±13.97% ±18.46%
readline/readline-iterable.js type='old' n=1000                    1.39 %       ±6.82%  ±9.14% ±12.03%
readline/readline-iterable.js type='old' n=10000          ***     18.40 %       ±3.62%  ±4.85%  ±6.38%
readline/readline-iterable.js type='old' n=100000         ***     26.02 %       ±2.91%  ±3.93%  ±5.21%
readline/readline-iterable.js type='old' n=1000000        ***     31.42 %       ±2.00%  ±2.67%  ±3.52%

Event-based consumption (rl.on('line')) goes from 8.1M to 13.3M lines/s (+65%) in a microbenchmark of 32-byte lines.

Speed up Interface construction:

- Hoist the history accessor property descriptors to module scope and
  define them with a single ObjectDefineProperties call, instead of
  allocating six closures and four descriptor objects per instance.
- Stop assigning the history options onto the input stream. This avoids
  hidden class transitions on the user provided stream and no longer
  mutates it observably.
- Only check process.env.TERM for a dumb terminal when the interface is
  in terminal mode. Reading process.env goes through the environment
  interceptor and is comparatively expensive, and _ttyWrite is never
  called when terminal is false.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. readline Issues and PRs related to the built-in readline module. labels Jul 18, 2026
Replace the per-line regular expression exec loop with a single split
pass, and use a plain string split on "\n" when the chunk contains none
of the rare line endings (\r, \u2028, \u2029). This avoids allocating a
match object and two index entries per line and lets the common case
use the fast string split path.

readline/readline-iterable.js type='new' improves by up to 80% and
the event based line consumption by up to 65%.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina mcollina changed the title readline: reduce createInterface overhead readline: improve performance Jul 18, 2026
@codecov

codecov Bot commented Jul 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.59155% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.12%. Comparing base (4f844f4) to head (0fd92a9).
⚠️ Report is 56 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/readline/interface.js 98.52% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #64585      +/-   ##
==========================================
- Coverage   90.13%   90.12%   -0.02%     
==========================================
  Files         741      741              
  Lines      241976   241994      +18     
  Branches    45543    45551       +8     
==========================================
- Hits       218116   218101      -15     
- Misses      15379    15410      +31     
- Partials     8481     8483       +2     
Files with missing lines Coverage Δ
lib/readline.js 95.80% <100.00%> (+0.01%) ⬆️
lib/internal/readline/interface.js 93.21% <98.52%> (+<0.01%) ⬆️

... and 30 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina requested review from anonrig and jasnell July 19, 2026 10:22
@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jul 20, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jul 20, 2026
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@trivikr trivikr added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Jul 21, 2026
@mcollina mcollina added the commit-queue Add this label to land a pull request using GitHub Actions. label Jul 24, 2026
@nodejs-github-bot nodejs-github-bot added commit-queue-failed An error occurred while landing this pull request using GitHub Actions. and removed commit-queue Add this label to land a pull request using GitHub Actions. labels Jul 24, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/64585
✔  Done loading data for nodejs/node/pull/64585
----------------------------------- PR info ------------------------------------
Title      readline: improve performance (#64585)
Author     Matteo Collina <matteo.collina@gmail.com> (@mcollina)
Branch     mcollina:readline-createinterface-perf -> nodejs:main
Labels     readline, author ready, needs-ci
Commits    2
 - readline: reduce createInterface overhead
 - readline: speed up line splitting in normal write
Committers 1
 - Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/64585
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/64585
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
--------------------------------------------------------------------------------
   ℹ  This PR was created on Sat, 18 Jul 2026 21:45:57 GMT
   ✔  Approvals: 5
   ✔  - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/64585#pullrequestreview-4731050691
   ✔  - Rafael Gonzaga (@RafaelGSS) (TSC): https://github.com/nodejs/node/pull/64585#pullrequestreview-4731061804
   ✔  - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/64585#pullrequestreview-4731326110
   ✔  - Benjamin Gruenbaum (@benjamingr) (TSC): https://github.com/nodejs/node/pull/64585#pullrequestreview-4731378980
   ✔  - Gürgün Dayıoğlu (@gurgunday): https://github.com/nodejs/node/pull/64585#pullrequestreview-4734487326
   ✔  Last GitHub CI successful
   ℹ  Last Full PR CI on 2026-07-20T14:46:25Z: https://ci.nodejs.org/job/node-test-pull-request/74905/
- Querying data for job/node-test-pull-request/74905/
✔  Build data downloaded
   ✔  Last Jenkins CI successful
--------------------------------------------------------------------------------
   ✔  No git cherry-pick in progress
   ✔  No git am in progress
   ✔  No git rebase in progress
--------------------------------------------------------------------------------
- Bringing origin/main up to date...
From https://github.com/nodejs/node
 * branch                  main       -> FETCH_HEAD
✔  origin/main is now up-to-date
- Downloading patch for 64585
From https://github.com/nodejs/node
 * branch                  refs/pull/64585/merge -> FETCH_HEAD
✔  Fetched commits as c558c8e81a7c..0fd92a96ca87
--------------------------------------------------------------------------------
[main 5fe0cee5e7] readline: reduce createInterface overhead
 Author: Matteo Collina <hello@matteocollina.com>
 Date: Sat Jul 18 23:45:16 2026 +0200
 3 files changed, 63 insertions(+), 27 deletions(-)
 create mode 100644 benchmark/readline/readline-createInterface.js
[main 07a3f277aa] readline: speed up line splitting in normal write
 Author: Matteo Collina <hello@matteocollina.com>
 Date: Sun Jul 19 00:03:28 2026 +0200
 1 file changed, 34 insertions(+), 26 deletions(-)
   ✔  Patches applied
There are 2 commits in the PR. Attempting autorebase.
(node:352) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
(Use `node --trace-deprecation ...` to show where the warning was created)
Rebasing (2/4)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
readline: reduce createInterface overhead

Speed up Interface construction:

  • Hoist the history accessor property descriptors to module scope and
    define them with a single ObjectDefineProperties call, instead of
    allocating six closures and four descriptor objects per instance.
  • Stop assigning the history options onto the input stream. This avoids
    hidden class transitions on the user provided stream and no longer
    mutates it observably.
  • Only check process.env.TERM for a dumb terminal when the interface is
    in terminal mode. Reading process.env goes through the environment
    interceptor and is comparatively expensive, and _ttyWrite is never
    called when terminal is false.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #64585
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>

[detached HEAD 7dd44d226b] readline: reduce createInterface overhead
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Jul 18 23:45:16 2026 +0200
3 files changed, 63 insertions(+), 27 deletions(-)
create mode 100644 benchmark/readline/readline-createInterface.js
Rebasing (3/4)
Rebasing (4/4)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
readline: speed up line splitting in normal write

Replace the per-line regular expression exec loop with a single split
pass, and use a plain string split on "\n" when the chunk contains none
of the rare line endings (\r, \u2028, \u2029). This avoids allocating a
match object and two index entries per line and lets the common case
use the fast string split path.

readline/readline-iterable.js type='new' improves by up to 80% and
the event based line consumption by up to 65%.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #64585
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>

[detached HEAD 1ba3425011] readline: speed up line splitting in normal write
Author: Matteo Collina <hello@matteocollina.com>
Date: Sun Jul 19 00:03:28 2026 +0200
1 file changed, 34 insertions(+), 26 deletions(-)
Successfully rebased and updated refs/heads/main.

ℹ Add commit-queue-squash label to land the PR as one commit, or commit-queue-rebase to land as separate commits.

https://github.com/nodejs/node/actions/runs/30086846509

avivkeller pushed a commit that referenced this pull request Jul 24, 2026
Speed up Interface construction:

- Hoist the history accessor property descriptors to module scope and
  define them with a single ObjectDefineProperties call, instead of
  allocating six closures and four descriptor objects per instance.
- Stop assigning the history options onto the input stream. This avoids
  hidden class transitions on the user provided stream and no longer
  mutates it observably.
- Only check process.env.TERM for a dumb terminal when the interface is
  in terminal mode. Reading process.env goes through the environment
  interceptor and is comparatively expensive, and _ttyWrite is never
  called when terminal is false.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #64585
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
@avivkeller

Copy link
Copy Markdown
Member

Landed in 9041ad0

@avivkeller avivkeller closed this Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author ready PRs that have at least one approval, no pending requests for changes, and a CI started. commit-queue-failed An error occurred while landing this pull request using GitHub Actions. needs-ci PRs that need a full CI run. readline Issues and PRs related to the built-in readline module.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants